package il.technion.cs236369.webserver;

import il.technion.cs236369.common.Common;
import il.technion.cs236369.exceptions.BaseDirDoesntExist;
import il.technion.cs236369.exceptions.FullQueueException;
import il.technion.cs236369.exceptions.InvalidConfig;
import il.technion.cs236369.exceptions.ParsingException;
import il.technion.cs236369.exceptions.SameExtensionDifferentHandler;
import il.technion.cs236369.exceptions.WelcomeFileDoesntExist;
import il.technion.cs236369.parsers.XMLToJson;
import il.technion.cs236369.threadPool.ThreadPool;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;

import javax.net.ServerSocketFactory;

import org.apache.http.HttpResponse;
import org.apache.http.impl.DefaultHttpServerConnection;
import org.apache.http.params.BasicHttpParams;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import com.google.inject.Guice;
import com.google.inject.Inject;
import com.google.inject.Injector;
import com.google.inject.name.Named;

/**
 * Web Server
 * 
 * @author gil
 * 
 */
public class WebServer {
	private static final int			POOL_TIMEOUT	= 7000;
	final private ServerSocketFactory	_srvSockFactory;
	final private int					_numSocketReaders;
	final private int					_numRequestHandlers;
	final private int					_sizeSocketQueue;
	final private int					_sizeRequestQueue;
	private WebServerConfiguration		_configuration;
	private ServerSocket				_serverSocket;
	private final ThreadPool			_socketReaderThreadPool;
	private final ThreadPool			_requestHandlerThreadPool;

	/**
	 * Constructs the WebServer
	 * 
	 * @param srvSockFactory The ServerSocketFactory to be used for creating a
	 *            single ServerSocket for listening for clients requests
	 * 
	 * @param port The port number to bounded by the ServerSocket
	 * 
	 * @param baseDir The base directory of the server in the file system
	 *            (absolute path)
	 * @param welcomeFile The file should be returned upon request of the base
	 *            dir
	 * @param numSocketReaders number of socket-reader threads
	 * @param numRequestHandlers number of request-handler threads
	 * @param sizeSocketQueue size of the socket queue (how many cliend sockets
	 *            are saved)
	 * @param sizeRequestQueue size of the socket queue (how many requests are
	 *            saved)
	 * @throws InvalidConfig when config.xml is invalid
	 * @throws BaseDirDoesntExist when base directory doesnt exist
	 * @throws WelcomeFileDoesntExist when welcome file doesnt exist
	 * @throws SameExtensionDifferentHandler when two different handlers defined
	 *             for one extension
	 * @throws InvocationTargetException
	 * @throws IllegalArgumentException
	 * @throws IllegalAccessException
	 * @throws InstantiationException
	 * @throws SecurityException
	 * @throws NoSuchMethodException
	 */
	@Inject
	WebServer(ServerSocketFactory srvSockFactory, @Named("httpserver.net.port") int port, @Named("httpserver.app.baseDir") String baseDir,
			@Named("httpserver.app.welcomeFile") String welcomeFile, @Named("httpserver.threads.numSocketReaders") int numSocketReaders,
			@Named("httpserver.threads.numRequestHandlers") int numRequestHandlers, @Named("httpserver.queues.sizeSocketQueue") int sizeSocketQueue,
			@Named("httpserver.queues.sizeRequestQueue") int sizeRequestQueue) throws InvalidConfig, BaseDirDoesntExist, WelcomeFileDoesntExist,
			SameExtensionDifferentHandler {
		//initialize variables
		_srvSockFactory = srvSockFactory;
		_serverSocket = null;
		_configuration = null;

		//initialize configuration
		try {
			WebServerConfiguration.init(port, baseDir, welcomeFile);
			_configuration = WebServerConfiguration.getInstance();
		} catch (BaseDirDoesntExist e1) {
			System.err.println("Base dir doesn't exist! " + baseDir);
			throw e1;
		} catch (WelcomeFileDoesntExist e1) {
			System.err.println("Welcome file doesn't exist! " + welcomeFile);
			throw e1;
		}
		_numSocketReaders = numSocketReaders;
		_numRequestHandlers = numRequestHandlers;
		_sizeSocketQueue = sizeSocketQueue;
		_sizeRequestQueue = sizeRequestQueue;

		//parse config.xml
		Collection<TypeHandlerContainer> typeHandlersCollection = new ArrayList<>();
		Map<String, String> mimeMap = new HashMap<>();
		File configFile = new File("config.xml");

		//parse configuration
		parseConfigXML(typeHandlersCollection, mimeMap, configFile);

		_configuration.setAllMimes(mimeMap);
		_configuration.setHandlers(typeHandlersCollection);

		//initialize thread pools
		_socketReaderThreadPool = new ThreadPool(_numSocketReaders, "Socket", POOL_TIMEOUT, _sizeSocketQueue);
		_requestHandlerThreadPool = new ThreadPool(_numRequestHandlers, "Request", POOL_TIMEOUT, _sizeRequestQueue);

		Common.getOut().println("Finished initialization.");
	}

	/**
	 * Create a new bounded server socket using ServerSocketFactory with the
	 * given port
	 * 
	 * @throws IOException unable to bind the server socket
	 */
	public void bind() throws IOException {
		// bind the server
		_serverSocket = _srvSockFactory.createServerSocket(_configuration.getPort());

	}

	/**
	 * Starts the server loop: listens to client requests and executes them.
	 */
	public void start() {
		long i = 0;
		//Connection Listener
		Common.getOut().println("Starting the server");
		while (true) {
			Socket insocket = null;
			try {
				Common.getOut().println(i + "Accepting connection...");
				//get a new connection
				insocket = _serverSocket.accept();
				Common.getOut().println(i + "Got a new connection...");
			} catch (IOException e1) {
				System.err.println(i + "Unable to accept incoming socket");
				continue;
			}
			try {
				//create a new socket reader to handle it
				Common.getOut().println(i + "Adding a new socket reading mission...");
				_socketReaderThreadPool.run(new SocketReader(insocket, _requestHandlerThreadPool), new TimeoutHandler(insocket));
				Common.getOut().println(i + "Mission added.");
			} catch (FullQueueException e) {
				//full queue handler
				sendfullSocketQueueHandlerResponse(insocket);
			}

			i++;
		}

	}

	public static void main(String[] args) throws Exception {
		// assume that config.xml is in the project main path
		Common.getOut().println("Starting...");
		Injector injector = Guice.createInjector(new WebServerModule());
		WebServer server = injector.getInstance(WebServer.class);
		server.bind();
		server.start();
	}

	private void sendfullSocketQueueHandlerResponse(Socket socket) {
		System.err.println("Socket Reader Queue is full!");
		HttpResponse response = RequestHandler.generateErrorResponse(503, "Service Unavailable", "Sever is busy");
		DefaultHttpServerConnection conn = new DefaultHttpServerConnection();
		try {
			conn.bind(socket, new BasicHttpParams());
			RequestHandler.sendResponseAndClose(socket, conn, response);
		} catch (IOException e) {
			System.err.println("Unable to bind connection");
		}
	}

	private void parseConfigXML(Collection<TypeHandlerContainer> typeHandlersCollection, Map<String, String> mimeMap, File configFile)
			throws InvalidConfig {
		Common.getOut().println("Pasing the config.xml...");
		try {
			XMLToJson parser = new XMLToJson();
			JSONObject config = parser.parse(configFile);
			JSONObject serverConfig = config.getJSONObject("server-config");
			JSONObject typeHandlers = serverConfig.getJSONObject("type-handlers");
			JSONObject mime = serverConfig.getJSONObject("mime");
			//type-handlers
			JSONArray typeHandlerArray = getArray(typeHandlers, "type-handler");
			for (int i = 0; i < typeHandlerArray.length(); i++) {
				JSONObject typeHandler = typeHandlerArray.getJSONObject(i);
				//Common.getOut().println("TypeHandler: "+typeHandler);
				Class<?> c = Class.forName(typeHandler.get("-class").toString());
				JSONArray parameterArray = getArray(typeHandler, "parameter");
				Map<String, String> parameters = getParamMap(parameterArray);
				JSONArray extensionArray = getArray(typeHandler, "extension");
				Collection<String> extensions = getExtensions(extensionArray);
				typeHandlersCollection.add(new TypeHandlerContainer(c, parameters, extensions));
			}
			//mime
			JSONArray mimeMappingArray = getArray(mime, "mime-mapping");
			for (int i = 0; i < mimeMappingArray.length(); i++) {
				JSONObject mimeMapping = mimeMappingArray.getJSONObject(i);
				//Common.getOut().println("MIME: "+mimeMapping);
				JSONObject extension = mimeMapping.getJSONObject("extension");
				JSONObject mimeType = mimeMapping.getJSONObject("mime-type");
				String ext = extension.getString("#text");
				String type = mimeType.getString("#text");
				mimeMap.put(ext, type);
			}
		} catch (FileNotFoundException | ParsingException | ClassNotFoundException | JSONException e) {
			throw new InvalidConfig(e);
		}
	}

	private Collection<String> getExtensions(JSONArray extensionArray) {
		ArrayList<String> result = new ArrayList<>();
		for (int i = 0; i < extensionArray.length(); i++) {
			JSONObject extension = extensionArray.getJSONObject(i);
			String extensionName = extension.getString("#text");
			result.add(extensionName);
		}
		return result;
	}

	private Map<String, String> getParamMap(JSONArray parameterArray) {
		Map<String, String> result = new HashMap<>();
		for (int i = 0; i < parameterArray.length(); i++) {
			JSONObject parameter = parameterArray.getJSONObject(i);
			String name = parameter.getString("-name");
			String value = parameter.getString("-value");
			result.put(name, value);
		}
		return result;
	}

	private JSONArray getArray(JSONObject obj, String entryName) {
		JSONArray result;
		if (obj.has(entryName + "Array")) {
			result = obj.getJSONArray(entryName + "Array");
		} else {
			result = new JSONArray();
			result.put(obj.getJSONObject(entryName));
		}
		assert (result != null);
		return result;
	}
}
